package com.tianji.promotion.service.impl;

import cn.hutool.core.bean.copier.CopyOptions;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.tianji.common.autoconfigure.mq.RabbitMqHelper;
import com.tianji.common.constants.MqConstants;
import com.tianji.common.domain.dto.PageDTO;
import com.tianji.common.exceptions.BadRequestException;
import com.tianji.common.exceptions.BizIllegalException;
import com.tianji.common.exceptions.DbException;
import com.tianji.common.utils.BeanUtils;
import com.tianji.common.utils.CollUtils;
import com.tianji.common.utils.NumberUtils;
import com.tianji.common.utils.UserContext;
import com.tianji.promotion.constants.PromotionConstants;
import com.tianji.promotion.domain.dto.CouponDiscountDTO;
import com.tianji.promotion.domain.dto.OrderCouponDTO;
import com.tianji.promotion.domain.dto.OrderCourseDTO;
import com.tianji.promotion.domain.po.Coupon;
import com.tianji.promotion.domain.po.CouponScope;
import com.tianji.promotion.domain.po.ExchangeCode;
import com.tianji.promotion.domain.po.UserCoupon;
import com.tianji.promotion.domain.query.UserCouponQuery;
import com.tianji.promotion.domain.vo.CouponVO;
import com.tianji.promotion.enums.ExchangeCodeStatus;
import com.tianji.promotion.enums.UserCouponStatus;
import com.tianji.promotion.mapper.CouponMapper;
import com.tianji.promotion.mapper.UserCouponMapper;
import com.tianji.promotion.mq.msg.UserCouponDTO;
import com.tianji.promotion.service.ICouponScopeService;
import com.tianji.promotion.service.IExchangeCodeService;
import com.tianji.promotion.service.IUserCouponService;
import com.tianji.promotion.strategy.discount.Discount;
import com.tianji.promotion.strategy.discount.DiscountStrategy;
import com.tianji.promotion.utils.CodeUtil;
import com.tianji.promotion.utils.PermuteUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.checkerframework.checker.units.qual.C;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.*;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.function.ToIntFunction;
import java.util.stream.Collectors;

import static com.tianji.promotion.constants.PromotionConstants.COUPON_CODE_MAP_KEY;
import static com.tianji.promotion.constants.PromotionConstants.COUPON_RANGE_KEY;

/**
 * <p>
 * 用户领取优惠券的记录，是真正使用的优惠券信息 服务实现类
 * </p>
 *
 * @author mxl
 * @since 2024-07-01
 */
@Service
@Slf4j
@RequiredArgsConstructor
public class UserCouponMqLuaServiceImpl extends ServiceImpl<UserCouponMapper, UserCoupon> implements IUserCouponService {

    private final CouponMapper couponMapper;

    private final IExchangeCodeService exchangeCodeService;

    private final StringRedisTemplate redisTemplate;

    private final RabbitMqHelper rabbitMqHelper;

    private final ICouponScopeService couponScopeService;

    private final Executor discountSolutionExecutor;

    private static final RedisScript<Long> RECEIVE_COUPON_SCRIPT;
    private static final RedisScript<String> EXCHANGE_COUPON_SCRIPT;


    static {
        RECEIVE_COUPON_SCRIPT = RedisScript.of(new ClassPathResource("lua/receive_coupon.lua"), Long.class);
        EXCHANGE_COUPON_SCRIPT = RedisScript.of(new ClassPathResource("lua/exchange_coupon.lua"), String.class);
    }

    // 使用lua脚本后无需加锁也是线程安全的
    @Override
    public void receiveCoupon(Long couponId) {
/*        // 1. 根据id查询优惠券信息， 做相关校验
        if(id == null){
            throw new BadRequestException("非法参数");
        }
        // 从redis获取优惠券信息
        Coupon coupon = queryCouponByCache(id);

        if(coupon == null){
            throw new BadRequestException("优惠券不存在");
        }
        LocalDateTime now = LocalDateTime.now();
        if(now.isBefore(coupon.getIssueBeginTime()) || now.isAfter(coupon.getIssueEndTime())){
            throw new BadRequestException("该优惠券不在发放时间");
        }
        if(coupon.getTotalNum() <= 0){
            throw new BadRequestException("该优惠券库存不足");
        }

        // 统计已领数量 校验是否超过限领数量
        Long userId = UserContext.getUser();
        String couponsKey = PromotionConstants.USER_COUPON_CACHE_KEY_PREFIX+id;
        // 代表领取之后的领取数量increment
        Long increment = redisTemplate.opsForHash().increment(couponsKey, userId.toString(), 1);
        if(increment > coupon.getUserLimit()){
            redisTemplate.opsForHash().increment(couponsKey, userId.toString(), -1);
            throw new BadRequestException("超出领取限制");
        }

        // 修改优惠券库存
        String key = PromotionConstants.COUPON_CACHE_KEY_PREFIX+id;
        redisTemplate.opsForHash().increment(key, "totoalNum", -1);*/

        // 1.执行LUA脚本，判断结果
        // 1.1.准备参数
        String key1 = PromotionConstants.COUPON_CACHE_KEY_PREFIX + couponId;
        String key2 = PromotionConstants.USER_COUPON_CACHE_KEY_PREFIX + couponId;
        Long userId = UserContext.getUser();
        // 1.2.执行脚本
        Long r = redisTemplate.execute(RECEIVE_COUPON_SCRIPT, List.of(key1, key2), userId.toString());
        int result = NumberUtils.null2Zero(r).intValue();
        if (result != 0) {
            // 结果大于0，说明出现异常
            throw new BizIllegalException(PromotionConstants.RECEIVE_COUPON_ERROR_MSG[result - 1]);
        }

        // 发送消息到mq
        UserCouponDTO message = new UserCouponDTO();
        message.setCouponId(couponId);
        message.setUserId(userId);

        rabbitMqHelper.send(MqConstants.Exchange.PROMOTION_EXCHAGE,
                MqConstants.Key.COUPON_RECEIVE_KEY,
                message);
    }

    /**
     * 从redis获取优惠券信息
     * @param id
     * @return
     */
    private Coupon queryCouponByCache(Long id) {
        // 1. 拼接key
        String key = PromotionConstants.COUPON_CACHE_KEY_PREFIX+id;
        // 2. 从redis获取数据
        Map<Object, Object> entries = redisTemplate.opsForHash().entries(key);
        Coupon coupon = BeanUtils.mapToBean(entries, Coupon.class, false, CopyOptions.create());
        return coupon;
    }

    @Override
    public void exchangeCode(String code) {
        // 1. 校验参数
        if(StringUtils.isBlank(code)){
            throw new BadRequestException("非法参数");
        }

        // 2. 解析兑换码得到id
        long serialId = CodeUtil.parseCode(code);

/*        // 3. 判断兑换码是否已经兑换 bitMap
        boolean result = exchangeCodeService.updateExchangeCodeMark(serialId, true);
        if(!result){
            throw new BizIllegalException("兑换码已经被使用");
        }
        String key = null;
        Long userId = UserContext.getUser();
        Long increment = null;
        try{
            // 4. 判断兑换码是否存在
            Long couponId = exchangeCodeService.exchangeTargetId(serialId);
//            ExchangeCode exchangeCode = exchangeCodeService.getById(serialId);
            if(couponId == null){
                throw new BadRequestException("兑换码不存在");
            }
            // 5. 判断是否过期
            LocalDateTime now = LocalDateTime.now();
            Coupon coupon = queryCouponByCache(couponId);

            if(now.isAfter(coupon.getIssueEndTime()) || now.isBefore(coupon.getIssueEndTime())){
                throw new BadRequestException("优惠券活动未开始或者已经结束");
            }
            // 6. 校验每人限领数量
            key = PromotionConstants.USER_COUPON_CACHE_KEY_PREFIX+couponId;
            increment = redisTemplate.opsForHash().increment(key, userId.toString(), 1);
            if(increment > coupon.getUserLimit()){
                throw new BadRequestException("超出领取数量");
            }*/
        // 2.执行LUA脚本
        Long userId = UserContext.getUser();
        String result = redisTemplate.execute(
                EXCHANGE_COUPON_SCRIPT,
                List.of(COUPON_CODE_MAP_KEY, COUPON_RANGE_KEY),
                String.valueOf(serialId), String.valueOf(serialId + 5000), userId.toString());
        long r = NumberUtils.parseLong(result);
        if (r < 10) {
            // 异常结果应该是在1~5之间
            throw new BizIllegalException(PromotionConstants.EXCHANGE_COUPON_ERROR_MSG[(int) (r - 1)]);
        }

        // 7. 发送消息到mq
        // 发送消息到mq
        UserCouponDTO message = new UserCouponDTO();
        message.setCouponId(r);
        message.setUserId(userId);
        message.setSerialNum(serialId);

        rabbitMqHelper.send(MqConstants.Exchange.PROMOTION_EXCHAGE,
                MqConstants.Key.COUPON_RECEIVE_KEY,
                message);


    }

    @Transactional
    @Override
    @Deprecated
    /**
     * 检查库存状态和用户领取状态，并更新
     * 以基于MQ进行了改进参见新方法 checkAndCreateUserCouponMQ
     */
    public void checkAndCreateUserCoupon(Long user, Coupon coupon, Long serialNum) {
        // 获取当前用户 对该优惠券 已领数量 user coupon 条件userid couponid 统计数量
        // 强制从常量池中取值 保证同一个用户不能超领
        if(coupon.getTotalNum() <= 0 || coupon.getIssueNum() >= coupon.getTotalNum()){
            throw new BadRequestException("该优惠券库存不足");
        }
        Integer count = this.lambdaQuery()
                .eq(UserCoupon::getUserId, user)
                .eq(UserCoupon::getCouponId, coupon.getId())
                .count();
        if(count != null && count >= coupon.getUserLimit()){
            throw new BadRequestException("已达到领取上限");
        }
        // 2. 优惠券的已发放数量+1
        int r = couponMapper.incrIssueNum(coupon.getId());
        if(r == 0){
            throw new BizIllegalException("优惠券库存不足");
        }

        // 3. 生成用户卷
        saveUserCoupon(user, coupon);

        // 4. 更新兑换码兑换状态
        if(serialNum != null){
            // 修改兑换码状态
            exchangeCodeService.lambdaUpdate()
                    .set(ExchangeCode::getStatus, ExchangeCodeStatus.USED)
                    .set(ExchangeCode::getUserId, user)
                    .eq(ExchangeCode::getId, serialNum)
                    .eq(ExchangeCode::getStatus, ExchangeCodeStatus.UNUSED)
                    .update();
        }
    }

    @Override
    public PageDTO<CouponVO> queryMyCoupon(UserCouponQuery query) {
        // 1. 参数校验
        if(query == null){
            throw new BadRequestException("参数不能为空");
        }
        // 2. 查询DB
        Long user = UserContext.getUser();
        Page<UserCoupon> page = lambdaQuery()
                .eq(query.getStatus() != null, UserCoupon::getStatus, query.getStatus())
                .eq(UserCoupon::getUserId, user)
                .page(query.toMpPageDefaultSortByCreateTimeDesc());

        List<UserCoupon> records = page.getRecords();
        if(CollUtils.isEmpty(records)){
            return PageDTO.empty(page);
        }
        // 3. 封装数据
        // 3.1 查询优惠券信息
        Set<Long> couponIds = records.stream().map(record -> record.getCouponId()).collect(Collectors.toSet());
        if(CollUtils.isEmpty(couponIds)){
            return PageDTO.empty(page);
        }
        List<Coupon> couponList = couponMapper.selectBatchIds(couponIds);
        // 3.2 po 转 vo
        Map<Long, CouponVO> couponVOMap = couponList.stream()
                .map(coupon -> BeanUtils.copyBean(coupon, CouponVO.class))
                .collect(Collectors.toMap(CouponVO::getId, c -> c));

        List<CouponVO> couponVOS = records.stream()
                .filter(record -> couponVOMap.get(record.getCouponId())!=null)
                .map(record -> couponVOMap.get(record.getCouponId()))
                .collect(Collectors.toList());

        return PageDTO.of(page, couponVOS);
    }

    @Override
    @Transactional
    public void checkAndCreateUserCouponMQ(UserCouponDTO msg) {
        // 1.从db中查询优惠券信息
        Long couponId = msg.getCouponId();
        Coupon coupon = couponMapper.selectById(couponId);
        if(coupon == null){
            return;
        }

        // 2. 优惠券的已发放数量+1
        int r = couponMapper.incrIssueNum(coupon.getId());
        if(r == 0){
           return;
        }

        // 3. 生成用户卷
        saveUserCoupon(msg.getUserId(), coupon);

        // 4. 更新兑换码兑换状态
        if(msg.getSerialNum() != null){
            // 修改兑换码状态
            exchangeCodeService.lambdaUpdate()
                    .set(ExchangeCode::getStatus, ExchangeCodeStatus.USED)
                    .set(ExchangeCode::getUserId, msg.getUserId())
                    .eq(ExchangeCode::getId, msg.getSerialNum())
                    .eq(ExchangeCode::getStatus, ExchangeCodeStatus.UNUSED)
                    .update();
        }
    }

    @Override
    public List<CouponDiscountDTO> findDiscountSolution(List<OrderCourseDTO> courses) {
        // 1. 查询用户可用优惠券 coupon 以及 usercoupon表
        Long userId = UserContext.getUser();
        List<Coupon> coupons = getBaseMapper().queryMyCoupon(userId);
        if(CollUtils.isEmpty(coupons)){
            return CollUtils.emptyList();
        }

        // 2. 初筛
        // 2.1 计算订单的金额，
        int totalAmount = courses.stream().mapToInt(OrderCourseDTO::getPrice).sum();
        // 2.2 校验优惠券是否可用
        List<Coupon> availableCoupons = coupons.stream()
                .filter(coupon -> DiscountStrategy.getDiscount(coupon.getDiscountType()).canUse(totalAmount, coupon))
                .collect(Collectors.toList());
        if(CollUtils.isEmpty(availableCoupons)){
            return CollUtils.emptyList();
        }

        // 3. 细筛  考虑优惠券的可用范围
        Map<Coupon, List<OrderCourseDTO>> avaMap = findAvailable(availableCoupons, courses);
        if(avaMap.isEmpty()){
            return CollUtils.emptyList();
        }
        availableCoupons = new ArrayList<>(avaMap.keySet()); // 真正的可用优惠券组合
        log.debug("经过细筛之后的，优惠券个数：{}", availableCoupons.size());

        // 4. 排列组合 计算每一种组合的优惠细则， 多线程并行计算
        for (Coupon availableCoupon : availableCoupons) {
            log.debug("优惠券： {} {}",
                    DiscountStrategy.getDiscount(availableCoupon.getDiscountType()).getRule(availableCoupon),
                    availableCoupon);
        }
        // 5. 筛选最优解
        List<List<Coupon>> solutions = PermuteUtil.permute(availableCoupons);
        availableCoupons.stream().forEach(coupon -> {
            List<Coupon> item = new ArrayList<>();
            item.add(coupon);
            solutions.add(item);
        });
        for (List<Coupon> solution : solutions) {
            List<Long> cids = solution.stream().map(Coupon::getId).collect(Collectors.toList());
            log.debug("{}", cids);
        }
        
        // 5. 开始计算每一种组合的优惠明细
//        log.debug("开始计算 每一种组合的优惠明细");
//        List<CouponDiscountDTO> dtos = new ArrayList<>();
//        for (List<Coupon> solution : solutions) {
//            CouponDiscountDTO dto = calculateSolutionDiscount(avaMap, solution, courses);
//            log.debug("方案最终优惠:{} 方案中使用了:{} 规则:{}", dto.getDiscountAmount(), dto.getIds(), dto.getRules());
//            dtos.add(dto);
//        }

        // 6. 使用多线程改造
        log.debug("多线程计算， 每一种组合的优惠明细");
        List<CouponDiscountDTO> dtoList = Collections.synchronizedList(new ArrayList<>(solutions.size()));
        CountDownLatch countDownLatch = new CountDownLatch(solutions.size());
        for (List<Coupon> solution : solutions) {
            CompletableFuture.supplyAsync(new Supplier<CouponDiscountDTO>() {
                @Override
                public CouponDiscountDTO get(){
                    CouponDiscountDTO dto = calculateSolutionDiscount(avaMap, solution, courses);
                    return dto;
                }
            }, discountSolutionExecutor).thenAccept(new Consumer<CouponDiscountDTO>() {
                @Override
                public void accept(CouponDiscountDTO dto) {
                    log.debug("方案最终优惠:{} 方案中使用了:{} 规则:{}", dto.getDiscountAmount(), dto.getIds(), dto.getRules());
                    dtoList.add(dto);
                    countDownLatch.countDown();
                }
            });
        }
        try {
            countDownLatch.await(2, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            log.error("多线程计算组合优惠明细失败：{}", e);
        }

        return findBestSolutions(dtoList);
    }

    /**
     *  计算优惠明细
     * @param orderCouponDTO 优惠券列表以及课程列表
     * @return
     */
    @Override
    public CouponDiscountDTO queryDiscountDetailByOrder(OrderCouponDTO orderCouponDTO) {
        // 1. 获取优惠券信息
        List<Long> userCouponIds = orderCouponDTO.getUserCouponIds();
        List<Coupon> couponList = getBaseMapper().selectCouponsByIdsAndStatus(userCouponIds, UserCouponStatus.UNUSED);

        // 2. 查询可用优惠券极其对应的课程列表 Map封装
        Map<Coupon, List<OrderCourseDTO>> avaMap = findAvailable(couponList, orderCouponDTO.getCourseList());
        // 3. 计算优惠金额
        return calculateSolutionDiscount(avaMap, couponList, orderCouponDTO.getCourseList());
    }

    @Override
    @Transactional
    public void writeOffCoupon(List<Long> userCouponIds) {
        // 1. 查询优惠券
        List<UserCoupon> userCoupons = this.lambdaQuery()
                .in(UserCoupon::getCouponId, userCouponIds)
                .list();
        if(CollUtils.isEmpty(userCoupons)){
            return;
        }
        // 3. 过滤无效券
        List<UserCoupon> list = userCoupons.stream()
                // 过滤无效券
                .filter(coupon -> {
                    if (coupon == null) {
                        return false;
                    }
                    if (UserCouponStatus.UNUSED != coupon.getStatus()) {
                        return false;
                    }
                    LocalDateTime now = LocalDateTime.now();
                    return !now.isBefore(coupon.getTermBeginTime()) && !now.isAfter(coupon.getTermEndTime());
                })
                // 组织新增数据
                .map(coupon -> {
                    UserCoupon c = new UserCoupon();
                    c.setId(coupon.getId());
                    c.setStatus(UserCouponStatus.USED);
                    return c;
                })
                .collect(Collectors.toList());
        // 2. 更新优惠券状态
        boolean success = updateBatchById(list);
        if (!success) {
            return;
        }
        // 3. 更新DB
        List<Long> couponIds = userCoupons.stream().map(UserCoupon::getCouponId).collect(Collectors.toList());
        int c = couponMapper.incrUsedNum(couponIds, 1);
        if (c < 1) {
            throw new DbException("更新优惠券使用数量失败！");
        }

    }

    @Override
    @Transactional
    public void refundCoupon(List<Long> userCouponIds) {
        // 1.查询优惠券
        List<UserCoupon> userCoupons = listByIds(userCouponIds);
        if (CollUtils.isEmpty(userCoupons)) {
            return;
        }
        // 2.处理优惠券数据
        List<UserCoupon> list = userCoupons.stream()
                // 过滤无效券
                .filter(coupon -> coupon != null && UserCouponStatus.USED == coupon.getStatus())
                // 更新状态字段
                .map(coupon -> {
                    UserCoupon c = new UserCoupon();
                    c.setId(coupon.getId());
                    // 3.判断有效期，是否已经过期，如果过期，则状态为 已过期，否则状态为 未使用
                    LocalDateTime now = LocalDateTime.now();
                    UserCouponStatus status = now.isAfter(coupon.getTermEndTime()) ?
                            UserCouponStatus.EXPIRED : UserCouponStatus.UNUSED;
                    c.setStatus(status);
                    return c;
                }).collect(Collectors.toList());

        // 4.修改优惠券状态
        boolean success = updateBatchById(list);
        if (!success) {
            return;
        }
        // 5.更新已使用数量
        List<Long> couponIds = userCoupons.stream().map(UserCoupon::getCouponId).collect(Collectors.toList());
        int c = couponMapper.incrUsedNum(couponIds, -1);
        if (c < 1) {
            throw new DbException("更新优惠券使用数量失败！");
        }
    }

    @Override
    public List<String> queryDiscountRules(List<Long> userCouponIds) {
        // 1.查询优惠券信息
        List<Coupon> coupons = baseMapper.selectCouponsByIdsAndStatus(userCouponIds, UserCouponStatus.USED);
        if (CollUtils.isEmpty(coupons)) {
            return CollUtils.emptyList();
        }
        // 2.转换规则
        return coupons.stream()
                .map(c -> DiscountStrategy.getDiscount(c.getDiscountType()).getRule(c))
                .collect(Collectors.toList());
    }

    /**
     * 求最优优惠策略
     * 用券少，省钱多
     * @param dtoList 优惠策略列表
     * @return
     */
    private List<CouponDiscountDTO> findBestSolutions(List<CouponDiscountDTO> dtoList) {
        // 1.准备Map记录最优解
        Map<String, CouponDiscountDTO> moreDiscountMap = new HashMap<>();       // 优惠券列表为key 比谁优惠的多 （券一样）
        Map<Integer, CouponDiscountDTO> lessCouponMap = new HashMap<>();        // 优惠金额为key比较谁短     （优惠金额一样）
        // 2.遍历，筛选最优解
        for (CouponDiscountDTO solution : dtoList) {
            // 2.1.计算当前方案的id组合
            String ids = solution.getIds().stream()
                    .sorted(Long::compare).map(String::valueOf).collect(Collectors.joining(","));
            // 2.2.比较用券相同时，优惠金额是否最大
            CouponDiscountDTO best = moreDiscountMap.get(ids);
            if (best != null && best.getDiscountAmount() >= solution.getDiscountAmount()) {
                // 当前方案优惠金额少，跳过
                continue;
            }
            // 2.3.比较金额相同时，用券数量是否最少
            best = lessCouponMap.get(solution.getDiscountAmount());
            int size = solution.getIds().size();
            if (size > 1 && best != null && best.getIds().size() <= size) {
                // 当前方案用券更多，放弃
                continue;
            }
            // 2.4.更新最优解
            moreDiscountMap.put(ids, solution);
            lessCouponMap.put(solution.getDiscountAmount(), solution);
        }
        // 3.求交集
        Collection<CouponDiscountDTO> bestSolutions = CollUtils
                .intersection(moreDiscountMap.values(), lessCouponMap.values());
        // 4.排序，按优惠金额降序
        return bestSolutions.stream()
                .sorted(Comparator.comparingInt(CouponDiscountDTO::getDiscountAmount).reversed())
                .collect(Collectors.toList());
    }

    /**
     * 计算每一个方案的优惠明细
     * @param avaMap 优惠券和可用课程的集合映射
     * @param solution 排列方式
     * @param courses 课程列表
     * @return
     */
    private CouponDiscountDTO calculateSolutionDiscount(Map<Coupon, List<OrderCourseDTO>> avaMap,
                                                        List<Coupon> solution,
                                                        List<OrderCourseDTO> courses) {
        // 1. 创建结果对象
        CouponDiscountDTO dto = new CouponDiscountDTO();
        // 2. 初始化 商品id和商品折扣明细的映射， 初始化折扣明细全设置为0
        Map<Long, Integer> detailMap = courses.stream().collect(Collectors.toMap(OrderCourseDTO::getId, orderCourseDTO -> 0));
        // 3. 计算该方案的折扣明细
        // 3.1 循环方案中的优惠券
        for (Coupon coupon : solution) {
            // 3.2 取出该优惠全对应的可用课程
            List<OrderCourseDTO> availableCourses = avaMap.get(coupon);
            // 3.3 计算可用课程的总金额 (商品价格减去优惠)
            int totalAmount = availableCourses.stream().mapToInt(new ToIntFunction<OrderCourseDTO>() {
                @Override
                public int applyAsInt(OrderCourseDTO value) {
                    return value.getPrice() - detailMap.get(value.getId());
                }
            }).sum();
            // 3.4 判断优惠券是否可用
            Discount discount = DiscountStrategy.getDiscount(coupon.getDiscountType());
            if(!discount.canUse(totalAmount, coupon)){
                continue;
            }
            // 3.5 计算该优惠券使用后的折扣值
            int discountAmount = discount.calculateDiscount(totalAmount, coupon);
            // 3.6 更新商品折扣明细
            calculateDetailDiscount(detailMap, availableCourses, totalAmount, discountAmount);
            // 3.7 累加每一个优惠券的优惠金额， 赋值给方案结果dto对象
            dto.getIds().add(coupon.getId());
            dto.getRules().add(discount.getRule(coupon));
            dto.setDiscountAmount(discountAmount + dto.getDiscountAmount());
        }
        return dto;
    }

    /**
     * 计算商品 折扣明细
     * @param detailMap 商品id与优惠金额映射
     * @param availableCourses 当前优惠券可用的课程
     * @param totalAmount 当前总金额
     * @param discountAmount 当前优惠金额
     */
    private void calculateDetailDiscount(Map<Long, Integer> detailMap,
                                         List<OrderCourseDTO> availableCourses,
                                         int totalAmount, int discountAmount) {
        // 用于计算优惠后，每个商品的优惠明细
        int time = 0;
        int remainDiscount = totalAmount;
        int discount = 0;
        for (OrderCourseDTO c : availableCourses) {
            ++time;
            if(time == availableCourses.size()){
                // 最后一个课程， 直接减去避免精度丢失
                discount = remainDiscount;
            }else {
                discount = c.getPrice() * discountAmount / totalAmount;
                remainDiscount -= discount;
            }
            detailMap.put(c.getId(), detailMap.get(c.getId())+discount);
        }
    }

    /**
     * 细筛 查询每一个优惠券 对应的可用课程
     * @param coupons  初筛之后的优惠券集合
     * @param orderCourseDTOS  订单中的课程集合
     * @return
     */
    private Map<Coupon, List<OrderCourseDTO>> findAvailable(List<Coupon> coupons,
                                                            List<OrderCourseDTO> orderCourseDTOS) {
        Map<Coupon, List<OrderCourseDTO>> map = new HashMap<>();
        // 1. 循环遍历初筛优惠券集合
        for (Coupon coupon : coupons) {
            // 2. 找出每一个优惠券的可用课程
            // 2.1 判断是否限定了范围
            List<OrderCourseDTO> availableCourses = orderCourseDTOS;
            if(coupon.getSpecific()){
                // 2.2 查询限定范围， 查询coupon scope表
                List<CouponScope> scopeList = couponScopeService.lambdaQuery().eq(CouponScope::getCouponId, coupon.getId()).list();
                // 2.3 得到限定分为的id集合
                List<Long> scopeIds = scopeList.stream().map(CouponScope::getBizId).collect(Collectors.toList());
                // 2.4 从orderCourses订单中所有的课程集合 删选该范围内的课程
                availableCourses = orderCourseDTOS.stream()
                        .filter(c -> scopeIds.contains(c.getCateId()))
                        .collect(Collectors.toList());
            }
            if(CollUtils.isEmpty(availableCourses)){
                continue;   // 无课程
            }
            // 3. 计算该优惠券可用课程的总金额
            int totalAmount = availableCourses.stream().mapToInt(OrderCourseDTO::getPrice).sum();
            Discount discount = DiscountStrategy.getDiscount(coupon.getDiscountType());
            // 4. 判断该优惠券是否可用
            if(discount.canUse(totalAmount, coupon)){
                map.put(coupon, availableCourses);
            }
        }
        return map;
    }

    // 保存用户卷
    private void saveUserCoupon(Long userId, Coupon coupon) {
        UserCoupon userCoupon = new UserCoupon();
        userCoupon.setUserId(userId).setCouponId(coupon.getId());
        LocalDateTime termEndTime = coupon.getTermEndTime();
        LocalDateTime termBeginTime = coupon.getTermBeginTime();
        if(termBeginTime == null && termEndTime == null){
            termBeginTime = LocalDateTime.now();
            termEndTime = termBeginTime.plusDays(coupon.getTermDays());
        }
        userCoupon.setTermBeginTime(termBeginTime);
        userCoupon.setTermEndTime(termEndTime);
        this.save(userCoupon);
    }
}
